﻿Imports System.ComponentModel
Imports System.Xml.Serialization

Namespace DataModel
    Public MustInherit Class BaseDataModel(Of T As {Class, New})
        Implements INotifyPropertyChanged, IDataErrorInfo

        <System.Xml.Serialization.XmlIgnore()>
        Public Overridable ReadOnly Property GetToolTip(propertyName As String) As String
            Get
                Try
                    For Each customAttribute As Object In Me.GetType().GetProperty(propertyName).GetCustomAttributes(GetType(DescriptionAttribute), True)
                        Return DirectCast(customAttribute, DescriptionAttribute).Description
                    Next
                Catch
                End Try
                Return ""
            End Get
        End Property

        <System.Xml.Serialization.XmlIgnore()>
        Public Overridable ReadOnly Property GetLabelText(propertyName As String) As String
            Get
                Try
                    For Each customAttribute As Object In Me.GetType().GetProperty(propertyName).GetCustomAttributes(GetType(DisplayNameAttribute), True)
                        Return DirectCast(customAttribute, DisplayNameAttribute).DisplayName
                    Next
                Catch ex As Exception

                End Try

                Return ""
            End Get
        End Property


        Protected Sub OnPropertyChanged(propertyName As String)
            ' If you do not have any listener do not create the PropertyChangedEventArgs object! 
            ' This will speed up the non ui implementations of the super classes.
            If PropertyChangedEvent IsNot Nothing Then
                RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
            End If
        End Sub
        Public Event PropertyChanged(sender As Object, e As PropertyChangedEventArgs) Implements INotifyPropertyChanged.PropertyChanged

        Protected Overridable Sub ClearAllError(ByVal fieldName As String)
            If ColumnErrors.ContainsKey(fieldName) Then
                ColumnErrors.Remove(fieldName)
            End If
        End Sub

        Protected Overridable Sub ClearError(ByVal fieldName As String, ByVal errorType As String)
            If ColumnErrors.ContainsKey(fieldName) Then
                Dim item As Dictionary(Of String, String) = ColumnErrors.Item(fieldName)
                If item Is Nothing Then
                    Return
                End If
                item.Remove(errorType)
                If item.Count > 0 Then
                    ColumnErrors.Item(fieldName) = item
                Else
                    ClearAllError(fieldName)
                End If

            End If
        End Sub

        Public Overridable Sub AddRangError(ByVal fieldName As String, ByVal lowerbound As String, ByVal upperBound As String)
            AddError(fieldName, "RangeError", String.Format("The value is out of range. The value must be between {0} - {1}.", lowerbound, upperBound))
        End Sub

        Public Overridable Sub AddError(ByVal fieldName As String, ByVal errorType As String, ByVal errorMessage As String)
            If ColumnErrors.ContainsKey(fieldName) Then
                Dim item As Dictionary(Of String, String) = ColumnErrors.Item(fieldName)
                If item Is Nothing Then
                    item = New Dictionary(Of String, String)
                End If
                If Not item.ContainsKey(errorType) Then
                    item.Add(errorType, errorMessage)
                    ColumnErrors.Item(fieldName) = item
                End If
            Else
                Dim item As Dictionary(Of String, String) = New Dictionary(Of String, String)
                item.Add(errorType, errorMessage)
                ColumnErrors.Item(fieldName) = item
            End If
        End Sub

        Public Overridable Function ColumnErrors() As Dictionary(Of String, Dictionary(Of String, String))
            If m_ColumnErrors Is Nothing Then
                m_ColumnErrors = New Dictionary(Of String, Dictionary(Of String, String))
            End If
            Return m_ColumnErrors
        End Function
        Private m_ColumnErrors As Dictionary(Of String, Dictionary(Of String, String))

        ''' <summary>
        ''' Get the name of the class
        ''' </summary>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Private Shared Function ClassName() As String
            If String.IsNullOrWhiteSpace(m_ClassName) Then
                m_ClassName = GetType(T).Name
            End If
            Return m_ClassName
        End Function
        Private Shared m_ClassName As String

        <XmlIgnore()>
        Public Overridable ReadOnly Property DataValidationErrorMessage() As String Implements IDataErrorInfo.Error
            Get
                ValidateDataObject()
                If ColumnErrors.Count > 0 Then
                    Dim errorList As New Text.StringBuilder
                    errorList.AppendFormat("Error In {0}", ClassName).AppendLine()
                    ColumnErrors.AsParallel.ForAll(Sub(item)
                                                       errorList.AppendFormat("Column: {0}, Error:{1}", item.Key, DataErrorInfoItem(item.Key)).AppendLine()
                                                   End Sub)
                    ValidationHasErrors = True

                    Return errorList.ToString
                Else

                    ValidationHasErrors = False
                    Return ""
                End If
            End Get
        End Property

        <DataObjectField(True),
         DisplayName("ValidationHasErrors"),
         Description("ValidationHasErrors"),
         XmlIgnore()>
        Public Property ValidationHasErrors() As Boolean
            Get
                Return m_ValidationHasErrors
            End Get
            Set(ByVal value As Boolean)
                If m_ValidationHasErrors = value Then
                    Return
                End If
                m_ValidationHasErrors = value
                OnPropertyChanged("ValidationHasErrors")
                RaiseEvent ValidationErrorsHasChanged(Me, EventArgs.Empty)
            End Set
        End Property
        Private m_ValidationHasErrors As Boolean
        Public Event ValidationErrorsHasChanged As EventHandler


        <XmlIgnore()>
        Public Overridable ReadOnly Property DataErrorInfoItem(ByVal columnName As String) As String Implements IDataErrorInfo.Item
            Get
                Dim returning As String
                ClearAllError(columnName)
                ValidateProperty(Me.GetType.GetProperty(columnName))
                If ColumnErrors.ContainsKey(columnName) Then
                    Dim sb As New List(Of String)
                    Dim item As Dictionary(Of String, String) = ColumnErrors.Item(columnName)
                    If item IsNot Nothing Then
                        For Each key As String In item.Keys
                            sb.Add(item.Item(key))
                        Next
                    End If

                    returning = String.Join(", ", sb.ToArray)
                Else
                    returning = ""
                End If
                ValidationHasErrors = ColumnErrors.Count > 0
                Return returning
            End Get
        End Property

        Public Event Validate As EventHandler
        Public Event Validated As EventHandler
        Public Shared Event ValidateObject As EventHandler
        Public Shared Event ValidatedObject As EventHandler

        Protected Sub ValidateDataObject()
            'Initialize the error collection
            ColumnErrors.Clear()

            'Enable calling the OnValidate event before validation takes place
            Try
                RaiseEvent Validate(Me, EventArgs.Empty)
                RaiseEvent ValidateObject(Me, EventArgs.Empty)
            Catch ex As Exception

            End Try

            Try
                For Each prop As System.Reflection.PropertyInfo In Me.GetType.GetProperties()
                    If prop.CanWrite AndAlso prop.PropertyType.Namespace.StartsWith("System") Then
                        ClearAllError(prop.Name)
                        ValidateProperty(Me.GetType.GetProperty(prop.Name))
                    End If
                Next

            Catch ex As Exception
                '
                '   Throw New Exception("Could not validate Object!", ex)
            Finally
                'Enable calling the OnValidated event after validation has taken place
                Try
                    RaiseEvent Validated(Me, EventArgs.Empty)
                    RaiseEvent ValidatedObject(Me, EventArgs.Empty)
                Catch ex As Exception

                End Try

            End Try
        End Sub

        Protected Sub ValidateProperty(propertyinfo As Reflection.PropertyInfo)
            If propertyinfo Is Nothing Then
                Return
            End If
            Try

                Dim data As Object = propertyinfo.GetValue(Me, Nothing)

                ' Set Default value if value is empty

                For Each customAttribute As Object In propertyinfo.GetCustomAttributes(GetType(DefaultValueAttribute), True)
                    If data Is Nothing Then
                        propertyinfo.SetValue(Me, TryCast(customAttribute, DefaultValueAttribute).Value, Nothing)
                        data = propertyinfo.GetValue(Me, Nothing)
                    End If
                Next

                ' Check if property value is required

                For Each customAttribute As Object In propertyinfo.GetCustomAttributes(GetType(DataAnnotations.RequiredAttribute), True)
                    If data Is Nothing OrElse (data IsNot Nothing AndAlso String.IsNullOrWhiteSpace(data.ToString)) Then
                        Dim attrib As DataAnnotations.RequiredAttribute = DirectCast(customAttribute, DataAnnotations.RequiredAttribute)
                        If attrib.AllowEmptyStrings = False Then
                            AddError(propertyinfo.Name, "Required", If(String.IsNullOrWhiteSpace(attrib.ErrorMessage), String.Format("{0} is required", propertyinfo.Name), attrib.ErrorMessage))
                        End If

                    End If
                Next

                ' Evaluate whether the property value lies within range
                For Each customAttribute As Object In propertyinfo.GetCustomAttributes(GetType(DataAnnotations.RegularExpressionAttribute), True)
                    Dim attrib As DataAnnotations.RegularExpressionAttribute = DirectCast(customAttribute, DataAnnotations.RegularExpressionAttribute)
                    If Not attrib.IsValid(data) Then
                        AddError(propertyinfo.Name, "RegEx Validation", If(String.IsNullOrWhiteSpace(attrib.ErrorMessage), String.Format("{0} is not a valid value", propertyinfo.Name), attrib.ErrorMessage))
                    End If
                Next

                For Each customAttribute As Object In propertyinfo.GetCustomAttributes(GetType(DataAnnotations.RangeAttribute), True)
                    Dim attrib As DataAnnotations.RangeAttribute = DirectCast(customAttribute, DataAnnotations.RangeAttribute)
                    If Not attrib.IsValid(data) Then
                        AddError(propertyinfo.Name, "Out of Range", If(Not String.IsNullOrWhiteSpace(attrib.ErrorMessage), String.Format("{0} is out of range", propertyinfo.Name), attrib.ErrorMessage))
                    End If
                Next
                For Each customAttribute As Object In propertyinfo.GetCustomAttributes(GetType(System.ComponentModel.DataAnnotations.StringLengthAttribute), True)
                    Dim maxLength As System.ComponentModel.DataAnnotations.StringLengthAttribute = DirectCast(customAttribute, System.ComponentModel.DataAnnotations.StringLengthAttribute)
                    If data Is Nothing Then
                        If (data IsNot Nothing AndAlso Not String.IsNullOrWhiteSpace(data.ToString)) Then
                            Dim value As String = data.ToString
                            If value.Length > maxLength.MaximumLength Then
                                AddError(propertyinfo.Name, "Max Length", If(String.IsNullOrWhiteSpace(maxLength.ErrorMessage), maxLength.ErrorMessage, String.Format("{0} Too many characters.", propertyinfo.Name)))
                            End If

                        End If
                    End If
                Next
            Catch ex As Exception

            End Try
        End Sub
    End Class
End Namespace